#!/usr/bin/perl
# vm-synchronize.pl
# Copyright (C) 2006, 2007  Stephane Alnet
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# 

#
# For more information: http://carrierclass.net/
#
# vm_synchronize
#   each server runs this to make sure it has the same content in its
#   message store as what the database says it should (the database is authoritative).

#
# Usage:
#
#   batch mode:         vm-synchronize.pl
#       Ran with no arguments, go through the database records to identify
#       potential changes.
#
#   selective mode:     vm-synchronize.pl <enterprise> <extension>
#       Ran with an enterprise and extension parameters, synchronize the specific
#       voicemail box.
#

use strict;
use warnings;
use lib '/home/manager/lib';
use CCNCore::IPC::Spool;
use CCNv2::Voicemail;
our $SCP_command = 'scp -B -q -o TCPKeepAlive=yes -o BatchMode=yes -o CheckHostIP=no -o VerifyHostKeyDNS=no ';


use Fcntl;
use GDBM_File;

use DBI;

use Sys::Syslog;

my $timer = 160;
sub catch_alarm {
    syslog 'err', "Alarm: timeout expired ($timer seconds).";
    closelog();
    exit 1;
}
$SIG{'ALRM'} = \&catch_alarm;
alarm($timer);

sub mwi_notify_phones
{
    our ($vmpath,$enterprise,$extension) = @_;

    return "Usage: vm_compare <vm-path> <enterprise> <extension>\n"
        unless defined $vmpath and defined $enterprise and defined $extension;

    return "Invalid vmpath $vmpath\n"
        unless -d $vmpath;
    return "Invalid enterprise $enterprise\n"
        unless -d "$vmpath/$enterprise";
    return "Invalid extension $extension\n"
        unless -d "$vmpath/$enterprise/$extension";

    return "MWI Notification failed\n" unless
    system('/usr/bin/perl',CCNCore::Home::config.'/local/bin/local-sipsak-notify.pl',$vmpath,$enterprise,$extension) == 0;
    return ''; # Success
}

# Usage:
#    vm_synchronize()   -- synchronizes based on information in the database
#    vm_synchronize($$) -- synchronizes based on enterprise+extension

sub vm_synchronize
{
    our $vmpath = '_{VMPATH}_';

    our $dbh;

    # Synchronize a single extension (including its subdirectories)
    # for voicemail messages.
    sub vm_synchronize_extension
    {
        our ($enterprise,$extension) = @_;

        my $exit = '';

        syslog 'info', "CCN-INFO vm_synchronize_extension($enterprise,$extension)";

        our $must_notify = 0;

        sub notify() { $must_notify = 1; return 1; }

        sub local_new($$$)
        {
            my ($identifier,$remote_path,$server) = @_;
            syslog 'info', "CCN-INFO local_new($identifier,$remote_path,$server)";

            my $dir = dirname $remote_path;

            mkdir "$vmpath/$dir";

            my $tmp_id = ".tmp.$$.".rand(10000);

            my $copy_sound =
            system("${SCP_command} -p 'root\@$server:$vmpath/$remote_path.WAV' '$vmpath/$dir/$tmp_id.WAV'");

            my $copy_text =
            system("${SCP_command} -p 'root\@$server:$vmpath/$remote_path.txt' '$vmpath/$dir/$tmp_id.txt'");

            if( $copy_sound == 0 and $copy_text == 0
                and -s "$vmpath/$dir/$tmp_id.WAV" > 0
                and -s "$vmpath/$dir/$tmp_id.txt" > 0 )
                # and the_md5_sum_of(the_content_of("$vmpath/$dir/$tmp_id.txt")) eq $identifier
            {
                my $move_sound;
                my $move_text;
                if( dir_lock("$vmpath/$dir") )
                {
                    # Critical section
                    my $id = new_id("$vmpath/$dir");
                    my $success =
                        rename("$vmpath/$dir/$tmp_id.WAV","$vmpath/$dir/$id.WAV")
                    and rename("$vmpath/$dir/$tmp_id.txt","$vmpath/$dir/$id.txt");

                    $success = 0
                        unless dir_unlock("$vmpath/$dir");

                    return "$dir/$id" if $success;

                    unlink "$vmpath/$dir/$id.txt";
                    unlink "$vmpath/$dir/$id.WAV";
                }
                else
                {
                    syslog 'info', "CCN-ERROR Locking of $dir failed";
                }
            }
            unlink "$vmpath/$dir/$tmp_id.WAV";
            unlink "$vmpath/$dir/$tmp_id.txt";
            return ''; # failure
        }

        sub local_move($$$)
        {
            my ($identifier,$from,$to) = @_;
            syslog 'info', "CCN-INFO local_move($identifier,$from,$to)";

            my $dir_from = dirname $from;
            my $dir_to   = dirname $to;

            mkdir "$vmpath/$dir_to";

            my $success = 0;

            my $id;

            if( dir_lock("$vmpath/$dir_from") )
            {
                if( dir_lock("$vmpath/$dir_to") )
                {
                    $id = new_id("$vmpath/$dir_to");
                    $success =
                        rename( "$vmpath/$from.WAV", "$vmpath/$dir_to/$id.WAV" )
                    and rename( "$vmpath/$from.txt", "$vmpath/$dir_to/$id.txt" );

                    $success = 0
                        unless dir_unlock("$vmpath/$dir_to");
                }
                else
                {
                    syslog 'info', "Locking of $vmpath/$dir_to failed";
                }
                dir_unlock("$vmpath/$dir_from");
            }
            else
            {
                syslog 'info', "CCN-INFO Locking of $vmpath/$dir_from failed";
            }
            return "$vmpath/$dir_to/$id" if $success;
            return ''; # failure
        }

        sub local_delete($$)
        {
            my ($identifier,$path) = @_;
            syslog 'info', "CCN-INFO local_delete($identifier,$path)";

            my $dir = dirname $path;

            my $success = 0;

            # Lock $vmpath/$path
            if( dir_lock("$vmpath/$dir") )
            {
                # Remove
                $success =
                    unlink( "$vmpath/$path.txt" )
                and unlink( "$vmpath/$path.WAV" );

                $success = 0
                    unless dir_unlock("$vmpath/$dir");
            }
            else
            {
                syslog 'info', "CCN-INFO Locking of $vmpath/$dir failed";
            }
            return $success;
        }

        our $sth_new = $dbh->prepare(
        q(INSERT INTO voicemail_message (enterprise,extension,state,identifier,last_seen_at,path)
            VALUE (?,?,?,?,?,?))
        )
            or return "CCN-ERROR sth_new: ".$dbh->errstr."\n";

        sub remote_new($$)
        {
            my ($identifier,$path) = @_;
            syslog 'info', "CCN-INFO remote_new($identifier,$path)";

            $dbh->begin_work() and
            $sth_new->execute($enterprise,$extension,'NEW',$identifier,'_{SERVER}_',$path)
            and notify() and $dbh->commit() and return 1;

            return 0;
        }

        our $sth_move = $dbh->prepare(
        q(UPDATE voicemail_message SET state = 'MOVED', path = ?, last_seen_at = ?
            WHERE enterprise = ? AND extension = ? AND identifier = ?)
        )
            or return "CCN-ERROR sth_move: ".$dbh->errstr."\n";

        sub remote_move($$)
        {
            my ($identifier,$path) = @_;
            syslog 'info', "CCN-INFO remote_move($identifier,$path)";

            $dbh->begin_work() and
            $sth_move->execute($path,'_{SERVER}_',$enterprise,$extension,$identifier)
            and notify() and $dbh->commit() and return 1;

            return 0;
        }

        our $sth_delete = $dbh->prepare(
        q(UPDATE voicemail_message SET state = 'DELETED'
            WHERE enterprise = ? AND extension = ? AND identifier = ?)
        )
            or return "CCN-ERROR sth_delete ".$dbh->errstr."\n";

        sub remote_delete($)
        {
            my ($identifier) = @_;
            syslog 'info', "CCN-INFO remote_delete($identifier)";

            $dbh->begin_work() and
            $sth_delete->execute($enterprise,$extension,$identifier)
            and notify() and $dbh->commit() and return 1;

            return 0;
        }


        # Parameter check
        return "Usage: vm_synchronize_extension <vm-path> <enterprise> <extension>\n"
            unless defined $vmpath and defined $enterprise and defined $extension;

        return "Invalid vmpath $vmpath\n"
            unless -d $vmpath;
        return "Invalid enterprise $enterprise\n"
            unless -d "$vmpath/$enterprise";

        our $db = "$vmpath/$enterprise/$extension";

        mkdir $db;  # OK if we fail -- mailbox will not exist if this is the first time it's used,
                    # but most of the time it will already exist.

        #
        my %local_changes = ();

        # %actual is used as the local reference
        my %actual;

        $exit = find_messages(\%actual,$vmpath,$enterprise,$extension);
        return $exit if $exit;

        # %state is only used for disambiguation
        my %state = ();
        my $gdbm_filename = "$db/.current.db";
        if( -e $gdbm_filename )
        {
            tie( %state, 'GDBM_File', $gdbm_filename, &GDBM_WRITER, 0660) or
                return "tie($gdbm_filename): $!\n";
        }
        else
        {
            tie( %state, 'GDBM_File', $gdbm_filename, &GDBM_WRCREAT, 0660) or
                return "tie($gdbm_filename): $!\n";
        }

        for my $identifier (keys %actual, keys %state)
        {
            # PRECONDITION: exists($actual{$identifier}) or exists $state{$identifier}
            $local_changes{$identifier} = 'NEW', next
                if exists $actual{$identifier} and not exists $state{$identifier};
            $local_changes{$identifier} = 'DELETED', next
                if not exists $actual{$identifier} and exists $state{$identifier};
            # ASSERT: exists($actual{$identifier}) and exists $state{$identifier}
            $local_changes{$identifier} = 'MOVED', next
                if dirname( $actual{$identifier} ) ne dirname( $state{$identifier} );

            $local_changes{$identifier} = 'NEW';
        }

        our $sth_sel_chg = $dbh->prepare('SELECT identifier, state, path, last_seen_at FROM voicemail_message WHERE enterprise = ? AND extension = ?');
        return "CCN-ERROR sth_sel_chg: ".$dbh->errstr."\n" unless $sth_sel_chg;

        # Select the files in voicemail_messages
        $sth_sel_chg->execute($enterprise,$extension)
            or return "SELECT Changes failed: $!\n";

        # Scan for files that should be present
        while( our ($identifier,$remote_change,$path,$last_seen_at) = $sth_sel_chg->fetchrow_array )
        {
            if( exists $local_changes{$identifier} )
            {
                my $local_change = $local_changes{$identifier};
                delete $local_changes{$identifier};

                $local_change = 'CHANGED'
                    if exists $actual{$identifier}
                    # This used to be  $path ne $actual{$identifier}  -- why?
                    and dirname( $path ) ne dirname( $actual{$identifier} );

                if( $remote_change eq 'DELETED' )
                {
                    next if $local_change eq 'DELETED';

                    delete $state{$identifier}, next
                        if local_delete($identifier,$actual{$identifier});
                    $exit .= "local_delete failed\n", next;
                }

                if( $local_change eq 'DELETED' )
                {
                    delete $state{$identifier}, next
                        if remote_delete($identifier);
                    $exit .= "remote_delete failed\n", next;
                }

                if( ($local_change eq 'CHANGED')
                or  ($local_change eq 'MOVED' and $remote_change eq 'NEW'))
                {
                    $state{$identifier} = $actual{$identifier}, next
                        if remote_move($identifier,$actual{$identifier});
                    $exit .= "remote_move failed\n", next;
                }

                if( $local_change eq 'NEW' and $remote_change eq 'MOVED' )
                {
                    my $result = local_move($identifier,$actual{$identifier},$path);
                    $state{$identifier} = $result, next
                        if $result ne '';
                    $exit .= "local_move failed\n", next;
                }

                next if $local_change eq $remote_change;

                $exit .= "Invalid combination (bad coverage): $local_change $remote_change\n";
            }
            else
            {
                # $local_change = 'ABSENT'
                next if $remote_change eq 'DELETED';
                my $result = local_new($identifier,$path,$last_seen_at);
                $state{$identifier} = $result, next
                    if $result ne '';
                $exit .= "local_new failed\n", next;
            }
        }

        # These were not found in the remote DB
        for my $identifier (keys %local_changes)
        {
            my $local_change = $local_changes{$identifier};
            next if $local_change eq 'DELETED';

            if( $local_change eq 'NEW' )
            {
                $state{$identifier} = $actual{$identifier}, next
                    if remote_new($identifier,$actual{$identifier});
                $exit .= "remote_new failed\n", next;
            }
            if( $local_change eq 'MOVED' )
            {
                $state{$identifier} = $actual{$identifier}, next
                    if remote_move($identifier,$actual{$identifier});
                $exit .= "remote_move failed\n", next;
            }
            $exit .= "Invalid combination (bad coverage): $local_change Absent\n";
        }

        untie %state;

        system "chown -R asterisk.asterisk '$vmpath/$enterprise/$extension'";

        sub actually_notify()
        {
            syslog 'info', 'CCN-INFO actually_notify()';

            my $sth_notify = $dbh->prepare(
                q(REPLACE INTO voicemail_update (enterprise,extension,server) VALUES (?,?,?);)
            );

            return "CCN-ERROR sth_notify prepare: ".$dbh->errstr."\n"
                if not $sth_notify;

            my $exit = '';

            # Database updates
            for my $server (qw(_{VM_SERVERS}_))
            {
                $exit .= "CCN-ERROR sth_notify execute: ".$dbh->errstr."\n"
                    if not $sth_notify->execute($enterprise,$extension,$server);
            }

            syslog 'info', 'CCN-INFO actually_notify() remote';

            # Spooled notifications
            my $spool = new CCNCore::IPC::Spool;
            for my $pk (qw(_{VM_SERVER_PKS}_))
            {
                $spool->spool_request_to($pk,qq(synchronize-vm $enterprise $extension));
            }

            return $exit;
        }

        $exit .= actually_notify() if $must_notify;

        # Send an update to the device
        $exit .= mwi_notify_phones($vmpath,$enterprise,$extension);

        return $exit;
    } # vm_synchronize_extension


    #
    # ----- Body of vm_synchronize()
    #

    my $dsn = "DBI:mysql:database=_{DB_NAME}_;host=_{DB_SERVER_IP}_";
    $dbh = DBI->connect($dsn, 'asterisk', '_{DB_PASS}_');

    syslog('err',"CCN-ERROR DBI connect: ".$DBI::errstr), return
        if not defined $dbh;

    my $sth_del = $dbh->prepare('DELETE FROM voicemail_update WHERE enterprise = ? AND extension = ? AND server = ?;');
    syslog('err',"CCN-ERROR sth_del: ".$dbh->errstr), $dbh->disconnect(), return
        unless $sth_del;

    # If parameters are provided, assume they are the enterprise and extension.
    if($#_ == 1)
    {
        my ($enterprise,$extension) = @_;
        # YYY This may be too restrictive?
        if( $enterprise =~ /^e\d+$/ and $extension =~ /^\d+$/ )
        {
            my $exit = vm_synchronize_extension($enterprise,$extension);
            if($exit)
            {
                syslog('err',$exit);
            }
            else
            {
                $sth_del->execute($enterprise,$extension,'_{SERVER}_');
                # It's OK to fail: there may not have been any pending updates.
            }
        }
        else
        {
            syslog('err',"CCN-ERROR invalid parameters");
        }
    }
    # Otherwise use the database to understand what we have to do.
    else
    {
        my $sth_sel = $dbh->prepare('SELECT enterprise, extension FROM voicemail_update WHERE server = ?;');
        if($sth_sel and $sth_sel->execute('_{SERVER}_'))
        {
            while( our ($enterprise,$extension) = $sth_sel->fetchrow_array )
            {
                my $exit = vm_synchronize_extension($enterprise,$extension);
                if($exit)
                {
                    syslog('err',$exit);
                }
                else
                {
                    $sth_del->execute($enterprise,$extension,'_{SERVER}_')
                    or syslog('err','CCN-ERROR Synchronization successful but db update failed: '.$dbh->errstr);
                }
            }
        }
        else
        {
            syslog('err',"CCN-ERROR sth_sel: ".$dbh->errstr);
        }
    }
    $dbh->disconnect();
    syslog('info','CCN-INFO Done.');
}

openlog 'vm-synchronize', 'pid cons', 'daemon';
vm_synchronize(@ARGV);
closelog();
exit 0;
